iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 29
0
自我挑戰組

TDD - 紅燈,綠燈,重構,30天 TDD之路有你有我系列 第 29

Day29 TDD套路經典!? Tennis Game!

  • 分享至 

  • xImage
  •  

----2019/04/03/16:57更新----
感謝 Teddy 大指出本文章的錯誤
http://teddy-chen-tw.blogspot.com/2019/02/tennis-kata.html?m=1

如文章內所述 IsDeuce() 的function內容是有問題的
若將本文內的 IsSameScore() 放進 IsDeuce()才是相較好的 (已於程式碼中更新)

當時應該是為了趕文章而沒有review過,就直接上了
趕進度的 Code、文章,品質一定會像今天這篇寫出的結果一樣差...(汗顏


這周周末就要去參加91開的極速開發的課了

有興趣可以看91大大表演XDDD(影片在此
https://www.youtube.com/watch?v=sC1Ruz-nWQg

想說最後一篇還是寫這一題感覺比較像有結尾的feel
會這麼說的原因是因為有某位學弟給我題目寫一題
但那個題目跟過去28天寫的題目都有點雷同,一樣是拆解題目分析然後解需求,所以我還覺得最後一題還是用這題來做結尾來得好XD

https://ithelp.ithome.com.tw/upload/images/20180115/2010720970Aks731Ls.jpg

這個套路比較好的原因是因為他比較像真的一般會遇到的事情

Tennis Game的需求就跟一般的規則一樣!

詳細在此
http://codingdojo.org/kata/Tennis/

好了,來吧!

首先我先用手寫了這個題目的需求測試案例

https://ithelp.ithome.com.tw/upload/images/20180115/20107209fUhHaazTNZ.jpg

一開始我們先來寫Love_All的測試吧!!

[TestMethod]
public void Love_All()
{
    var tennisGame = new TennisGame();
    var score = tennisGame.Score();
    Assert.AreEqual("Love-All",score);
}

而Production Code 也就是老樣子會長成這個樣子

public string Score()
{
    throw new System.NotImplementedException();
}

老樣子,跑個測試,沒過很正常,紅燈,commit一下

接下來把Production Code改成這個樣子

public string Score()
{
    return "Love-All";
}

接下來跑個測試,PASS! Commit~
Test code Refactor他一下

TennisGame tennisGame = new TennisGame();
[TestMethod]
public void Love_All()
{
    ScoreShouldBe("Love-All");
}

private void ScoreShouldBe(string expected)
{
    var score = tennisGame.Score();
    Assert.AreEqual(expected, score);
}

再來新增一個Fifteen_Love的測試

[TestMethod]
public void Fifteen_Love()
{
    tennisGame.SetFirstPlayerScore(1);
    ScoreShouldBe("Fifteen-Love");
}

通過他的Production Code就需要增加第一個玩家的分數變數了

public class TennisGame
{

    private int _firstPlayerScore;

    public string Score()
    {
        if (_firstPlayerScore == 1)
        {
            return "Fifteen-Love";
        }
        return "Love-All";
    }

    public void SetFirstPlayerScore(int n)
    {
        _firstPlayerScore = n;
    }
}

接下來新增第二個測試 Thirty_Love

[TestMethod]
public void Thirty_Love()
{
    tennisGame.SetFirstPlayerScore(2);
    ScoreShouldBe("Thirty-Love");
}

加上一個判斷是就可以通過測試了

public string Score()
{
    if (_firstPlayerScore == 2)
    {
        return "Thirty-Love";
    }
    if (_firstPlayerScore == 1)
    {
        return "Fifteen-Love";
    }
    return "Love-All";
}

看到這樣重複的Code就發現其實是可以Refactor他的
這時候看起來是可以用Dictionary!
所以完整的Production Code變成這個樣子

public class TennisGame
{

    private int _firstPlayerScore;

    private readonly Dictionary<int, string> _scoreDictionary = new Dictionary<int, string>
    {
        {1, "Fifteen" },
        {2, "Thirty" }
    };

    public string Score()
    {

        if (_firstPlayerScore > 0)
        {
            return _scoreDictionary[_firstPlayerScore] + "-Love";
        }
        return "Love-All";
    }

    public void SetFirstPlayerScore(int n)
    {
        _firstPlayerScore = n;
    }
}

現在就來加入Forty_Love的測試!

[TestMethod]
public void Forty_Love()
{
    tennisGame.SetFirstPlayerScore(3);
    ScoreShouldBe("Forty-Love");
}

因為剛才Refactor的緣故所以只要在Dictionary加上一個key即可通過測試

private readonly Dictionary<int, string> _scoreDictionary = new Dictionary<int, string>
{
    {1, "Fifteen" },
    {2, "Thirty" },
    {3, "Forty" }
};

接下來要考慮到第二個玩家的分數囉
所以新增Love_Fifteen的測試案例

[TestMethod]
public void Love_Fifteen()
{
    tennisGame.SetSecondPlayerScore(1);
    ScoreShouldBe("Love-Fifteen");
}

Love_Fifteen這個測試案例就要新增Dictionary 1個0的Key是Love
也要將第2個人的分數考慮進去,所以所有Production Code變成這個樣子。

public class TennisGame
{

    private int _firstPlayerScore;
    private int _secondPlayerScore;

    private readonly Dictionary<int, string> _scoreDictionary = new Dictionary<int, string>
    {
        {0, "Love"},
        {1, "Fifteen"},
        {2, "Thirty"},
        {3, "Forty"}
    };

    public string Score()
    {
        if (_firstPlayerScore > 0 || _secondPlayerScore > 0)
        {
            return _scoreDictionary[_firstPlayerScore] + "-" + _scoreDictionary[_secondPlayerScore];
        }
        return "Love-All";
    }

    public void SetFirstPlayerScore(int n)
    {
        _firstPlayerScore = n;
    }

    public void SetSecondPlayerScore(int n)
    {
        _secondPlayerScore = n;
    }
}

改成這個樣子時Love_Thirty跟Love_Forty的測試也等同於通過了。

 [TestMethod]
public void Love_Thirty()
{
    tennisGame.SetSecondPlayerScore(2);
    ScoreShouldBe("Love-Thirty");
}

[TestMethod]
public void Love_Forty()
{
    tennisGame.SetSecondPlayerScore(3);
    ScoreShouldBe("Love-Forty");
}

再來寫平手的時候的測試吧!
Fifteen_All

[TestMethod]
public void Fifteen_All()
{
    tennisGame.SetFirstPlayerScore(1);
    tennisGame.SetSecondPlayerScore(1);
    ScoreShouldBe("Fifteen-All");
}

Production Code就會加上一個判斷並回傳,這樣基本上第二個平手測試Thirty_All也會通過了。

public string Score()
{
    if (_firstPlayerScore == _secondPlayerScore)
    {
        return _scoreDictionary[_firstPlayerScore] + "-All";
    }

    if (_firstPlayerScore > 0 || _secondPlayerScore > 0)
    {
        return _scoreDictionary[_firstPlayerScore] + "-" + _scoreDictionary[_secondPlayerScore];
    }

    return "Love-All";
}

這裡會發現最後的Love-All是多餘的
所以其實上述的兩個If是可以合再一起的
來Refactor一下

public string Score()
{
    if (_firstPlayerScore == _secondPlayerScore)
    {
        return _scoreDictionary[_firstPlayerScore] + "-All";
    }
    return _scoreDictionary[_firstPlayerScore] + "-" + _scoreDictionary[_secondPlayerScore];

}

再來寫Deuce的這兩個測試。

[TestMethod]
public void Deuce()
{
    tennisGame.SetFirstPlayerScore(3);
    tennisGame.SetSecondPlayerScore(3);
    ScoreShouldBe("Deuce");
}

[TestMethod]
public void GetHour_Input_3600_Should_Be_1()
{
    Assert.AreEqual(1, TimeFormat.GetHour(3600));
}

只要加入一個判斷即可通過測試,這個測試基本上也通過了4:4的測試了

public string Score()
{
    if (_firstPlayerScore == _secondPlayerScore)
    {
        if (_firstPlayerScore >= 3)
        {
            return "Deuce";
        }
        return _scoreDictionary[_firstPlayerScore] + "-All";
    }
    return _scoreDictionary[_firstPlayerScore] + "-" + _scoreDictionary[_secondPlayerScore];
}

4:4的測試Code我會這麼寫

[TestMethod]
public void When_4_4_Then_Deuce()
{
    tennisGame.SetFirstPlayerScore(4);
    tennisGame.SetSecondPlayerScore(4);
    ScoreShouldBe("Deuce");
}

再來加入Deuce之後領先的人的測試了!
因為要考慮到人名了,所以測試Code上方的TennisGame Class要加入人名的建構式了。(這樣就不用再加裡面所有的測試Code了呢

TennisGame tennisGame = new TennisGame("Lin","DZ");
[TestMethod]
public void Lin_Adv()
{
    tennisGame.SetFirstPlayerScore(4);
    tennisGame.SetSecondPlayerScore(3);
    ScoreShouldBe("Lin Adv");
}

而Production Code就會長這個樣子

public string Score()
{
    if (_firstPlayerScore == _secondPlayerScore)
    {
        if (_firstPlayerScore >= 3)
        {
            return "Deuce";
        }
        return _scoreDictionary[_firstPlayerScore] + "-All";
    }

    if (_firstPlayerScore >= 3 && _secondPlayerScore >= 3)
    {
        if (Math.Abs(_firstPlayerScore - _secondPlayerScore) == 1)
        {
            return _firstPlayerName + " Adv";
        }
    }
    return _scoreDictionary[_firstPlayerScore] + "-" + _scoreDictionary[_secondPlayerScore];
}

再來寫第1個玩家獲勝的測試

[TestMethod]
public void Lin_Win()
{
    tennisGame.SetFirstPlayerScore(5);
    tennisGame.SetSecondPlayerScore(3);
    ScoreShouldBe("Lin Win");
}

完成了這個測試之後再新增一個第2個玩家Adv的測試

[TestMethod]
public void DZ_Adv()
{
    tennisGame.SetFirstPlayerScore(3);
    tennisGame.SetSecondPlayerScore(4);
    ScoreShouldBe("DZ Adv");
}

通過之後就可以快速地更改Production Code並更改第2個玩家Win的測試

[TestMethod]
public void DZ_Win()
{
    tennisGame.SetFirstPlayerScore(3);
    tennisGame.SetSecondPlayerScore(5);
    ScoreShouldBe("DZ Win");
}

Production Code就會變成這個樣子

public string Score()
{
    if (_firstPlayerScore == _secondPlayerScore)
    {
        if (_firstPlayerScore >= 3)
        {
            return "Deuce";
        }
        return _scoreDictionary[_firstPlayerScore] + "-All";
    }

    if (_firstPlayerScore > 3 || _secondPlayerScore > 3)
    {
        var advPlayer = _firstPlayerScore > _secondPlayerScore ? _firstPlayerName : _secondPlayerName;

        if (Math.Abs(_firstPlayerScore - _secondPlayerScore) == 1)
        {
            return advPlayer + " Adv";
        }
        return advPlayer + " Win";
    }

    return _scoreDictionary[_firstPlayerScore] + "-" + _scoreDictionary[_secondPlayerScore];
}

接下來就一口氣Refactor 全部的Code超過癮的
(其實在實作過程中就要Refactor了,只是今天想要這麼做XD)

這是今天的所有測試案例

[TestClass]
public class UnitTest1
{
    TennisGame tennisGame = new TennisGame("Lin","DZ");
    [TestMethod]
    public void Love_All()
    {
        ScoreShouldBe("Love-All");
    }

    [TestMethod]
    public void Fifteen_Love()
    {
        tennisGame.SetFirstPlayerScore(1);
        ScoreShouldBe("Fifteen-Love");
    }

    [TestMethod]
    public void Thirty_Love()
    {
        tennisGame.SetFirstPlayerScore(2);
        ScoreShouldBe("Thirty-Love");
    }

    [TestMethod]
    public void Forty_Love()
    {
        tennisGame.SetFirstPlayerScore(3);
        ScoreShouldBe("Forty-Love");
    }

    [TestMethod]
    public void Love_Fifteen()
    {
        tennisGame.SetSecondPlayerScore(1);
        ScoreShouldBe("Love-Fifteen");
    }

    [TestMethod]
    public void Love_Thirty()
    {
        tennisGame.SetSecondPlayerScore(2);
        ScoreShouldBe("Love-Thirty");
    }

    [TestMethod]
    public void Love_Forty()
    {
        tennisGame.SetSecondPlayerScore(3);
        ScoreShouldBe("Love-Forty");
    }

    [TestMethod]
    public void Fifteen_All()
    {
        tennisGame.SetFirstPlayerScore(1);
        tennisGame.SetSecondPlayerScore(1);
        ScoreShouldBe("Fifteen-All");
    }

    [TestMethod]
    public void Thirty_All()
    {
        tennisGame.SetFirstPlayerScore(2);
        tennisGame.SetSecondPlayerScore(2);
        ScoreShouldBe("Thirty-All");
    }

    [TestMethod]
    public void Deuce()
    {
        tennisGame.SetFirstPlayerScore(3);
        tennisGame.SetSecondPlayerScore(3);
        ScoreShouldBe("Deuce");
    }

    [TestMethod]
    public void When_4_4_Then_Deuce()
    {
        tennisGame.SetFirstPlayerScore(4);
        tennisGame.SetSecondPlayerScore(4);
        ScoreShouldBe("Deuce");
    }

    [TestMethod]
    public void Lin_Adv()
    {
        tennisGame.SetFirstPlayerScore(4);
        tennisGame.SetSecondPlayerScore(3);
        ScoreShouldBe("Lin Adv");
    }

    [TestMethod]
    public void Lin_Win()
    {
        tennisGame.SetFirstPlayerScore(5);
        tennisGame.SetSecondPlayerScore(3);
        ScoreShouldBe("Lin Win");
    }

    [TestMethod]
    public void DZ_Adv()
    {
        tennisGame.SetFirstPlayerScore(3);
        tennisGame.SetSecondPlayerScore(4);
        ScoreShouldBe("DZ Adv");
    }

    [TestMethod]
    public void DZ_Win()
    {
        tennisGame.SetFirstPlayerScore(3);
        tennisGame.SetSecondPlayerScore(5);
        ScoreShouldBe("DZ Win");
    }

    private void ScoreShouldBe(string expected)
    {
        var score = tennisGame.Score();
        Assert.AreEqual(expected, score);
    }
}

這是今天所有的Production Code
全部都攤平平的,看起來很過癮XDDD

public class TennisGame
{

    private int _firstPlayerScore;
    private int _secondPlayerScore;
    private string _firstPlayerName;
    private string _secondPlayerName;
    private readonly Dictionary<int, string> _scoreDictionary = new Dictionary<int, string>
    {
        {0, "Love"},
        {1, "Fifteen"},
        {2, "Thirty"},
        {3, "Forty"}
    };

    public TennisGame(string firstPlayerName, string secondPlayerName)
    {
        this._firstPlayerName = firstPlayerName;
        this._secondPlayerName = secondPlayerName;
    }

    public string Score()
    {
        return IsSameScore()
            ? (IsDeuce() ? Deuce() : SameScore())
            : (ReadyForWin() ? AdvOrWinPlayer() : NormalScore());
    }

    private bool IsSameScore()
    {
        return _firstPlayerScore == _secondPlayerScore;
    }

    private static string Deuce()
    {
        return "Deuce";
    }

    private bool IsDeuce()
    {
        return IsSameScore() && _firstPlayerScore >= 3; //更新於 2019/04/03 16:50
    }

    private string SameScore()
    {
        return _scoreDictionary[_firstPlayerScore] + "-All";
    }

    private string AdvOrWinPlayer()
    {
        return AdvPlayer() + (IsAdv() ? " Adv" : " Win");
    }

    private bool ReadyForWin()
    {
        return _firstPlayerScore > 3 || _secondPlayerScore > 3;
    }

    private string AdvPlayer()
    {
        return _firstPlayerScore > _secondPlayerScore ? _firstPlayerName : _secondPlayerName;
    }

    private bool IsAdv()
    {
        return Math.Abs(_firstPlayerScore - _secondPlayerScore) == 1;
    }

    private string NormalScore()
    {
        return _scoreDictionary[_firstPlayerScore] + "-" + _scoreDictionary[_secondPlayerScore];
    }


    public void SetFirstPlayerScore(int n)
    {
        _firstPlayerScore = n;
    }

    public void SetSecondPlayerScore(int n)
    {
        _secondPlayerScore = n;
    }
}

今天就是最後一篇TDD的練習啦!
感謝各位收看
今天超趕的,好可怕
因為有重寫這一篇
不過好險有趕上!!!

最後還一口氣Refactor了全部的Code很過癮!!!


上一篇
Day28. 把塔蓋起來蓋起來!! Codewars_Build Tower
下一篇
Day30. 最後一天就是要Retro一下啊!
系列文
TDD - 紅燈,綠燈,重構,30天 TDD之路有你有我30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
nolions
iT邦新手 5 級 ‧ 2018-11-15 17:09:10

是不是有少寫Advantage的情境啊

張少齊 iT邦新手 5 級 ‧ 2019-04-03 16:47:30 檢舉

應該是有 IsAdv中就是了

我要留言

立即登入留言